[JavaScript] V8 和 Event Loop

前言

最近研究了下 V8 和 Event Loop,以下圖片和程式碼皆引用自影片中。

V8

以往JavaScript是透過直譯式的方式執行,而 V8 會直接將 JavaScript 轉換成電腦看的懂的Machine Code 再執行,目前在 Chrome 和 Node.js 都是使用這套引擎。


Hidden Class

JavaScript是動態語言,這帶來了便利但也造成效能問題,以往類似引擎會採用類似Hash Table的方式來編譯,而V8則會在Runtime的時候建立Hidden Class

若物件有相同的Hidden Class,則可以使用相同的Machine Code,稱之為Inline Caches


依照程式的執行順序,我們會依序創建Hidden Class

p1:
Point->Point,x->Point,x,y
p2:
由於創建過所以可以直接指到Point,x,y,這時候如果我們給p2一個新的值z,則會創建一個Point,x,y,z

優化

  1. 經由構造函數創建所有的物件
  2. 使用同順序創建物件的元素

數字


V8會用32 bits的空間
用最後一個bit來判別是不是數字,1為物件指標而0則為數字。

萬一這個數字超過31個bits,這時候會將數字放在Box裡並轉換為double,再存到物件中。

優化

  • 使用31 bits的有符號整數

陣列

Fast Elements

緊密的陣列會使用線性的儲存。

Dictionary Elements

寬鬆的陣列會使用Hash Table儲存。

優化

  1. 使用連續的陣列並且起始為0
  2. 不要宣告一個過大的陣列
  3. 別刪除陣列的元素
  4. 別使用未宣告或者已刪除的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //bad
    var a = new Array();
    for ( var b = 0 ; b < 10 ; b ++ ){
    a[0] |= b ;
    }
    //good
    var a = new Array();
    a[0] = 0 ;
    for ( var b = 0 ; b < 10 ; b ++ ){
    a[0] |= b ;
    }
  5. 若陣列中值的類型都是Double,陣列會將Double unbox且直接存在Double類型的buffer。

  6. 若陣列的元素類型不一致會產生不同的Hidden Class,因此造成效能上的花費。
  7. 事先宣告可在compile時讓V8優化

    1
    var a = [ 77 , 88 , 0.5 , true ] ;
  8. 小陣列中事先宣告正確大小


Compilers

V8有兩種Compilers。

Full Compiler

  1. 盡快產生可以執行的程式
  2. Compile Time幾乎不做類別分析
  3. 使用Inline CachesRuntime做類別分析且最佳化,同樣的Hidden Class可以使用同樣的最佳化程式。
    1
    2
    3
    4
    5
    function add( x , y ){
    return x + y ;
    }
    add(1,2); // Monomorphic
    add("a","b"); // Polymorphic

Optimizing Compiler

  1. Optimizing Compiler會收集Inline Caches的資訊來對於常使用的函式重新編譯。
  2. try/catch區塊無法最佳化 (影片為2012年,不曉得目前是否仍是如此)
  3. 若要使用try/catch則使用下列方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function perf(){
    // do work here
    }
    try {
    perf();
    } catch (e){
    // handle exceptions here
    }

反最佳化

若 V8 發現最佳化的效果不佳會自動反最佳化,如此一來會造成效能上的損失。

優化

  • 不要改變最佳化的function造成Hidden Class的改變。

Demo

1
2
3
4
5
6
7
this.isPrimeDivisible = function(candidate){
for ( var i = 1 ; i <= this.prime_count ; ++i ){
if ( candidate % this.primes[i] === 0 )
return true ;
}
return false ;
}

這段程式碼中由於超出邊界,如此一來造成效能上的影響

修正邊界後

演算法上的影響也很重要。

1
2
3
4
5
6
7
8
9
10
11
this.isPrimeDivisible = function(candidate){
for ( var i = 1 ; i <= this.prime_count ; ++i ){
var current_prime = this.primes[i] ;
if ( current_prime * current_prime > candidate ){
return false ;
}
if ( candidate % current_prime === 0 )
return true ;
}
return false ;
}

改進後的效能差異


單執行緒

由於起初做為瀏覽器的語言,JavaScript被設計為單執行緒,如此才不會在多執行緒的情況下造成 DOM 操作上的問題。

HTML5的Web Worker可以另外建立執行緒,但新的執行緒仍不能操作 DOM。

Event Loop

現在知道了JavaScript是如何編譯運行的,那JavaScript是如何處理異步事件像是DOM、HTTP Request、Timer等等呢?


前面提到的V8就負責了heapstack操作,那些WebAPIs則不包含在V8裡頭。 (瀏覽器端的WebAPIs和Node.js的API不同,但Event Loop原理是差不多的。)

單執行緒代表了只有一個call stack,也代表了一次只能做一件事情。

而其他API的事件則會經由其他的執行緒來運行,等執行完成再觸發callback。

下列一段程式碼在stack的情形。


stack 的情形也常在 console 中看到。

無窮遞迴的情形。


Task Queue

task queue用來儲存需要執行的程式。

setTimeout 經由 API 在別的執行緒進行。

一但 API 執行完,則把 callback 放回task queue等待運行

這時stack中的函式可能還在運行或者已完成,但要等到stack中的函式運行完才將task queue的任務放進 stack 運行


DOM事件的監聽情形。

按下按鈕後

逐步執行


參考

Google I/O 2012 - Breaking the JavaScript Speed Limit with V8
Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014
Philip Roberts: Help, I’m stuck in an event-loop
V8’s public wiki